// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: jhkim

#include <boost/algorithm/string.hpp>
#include <cxxopts.hpp>

#include "appcoin2/support/feed/fastfeed_util.h"
#include "appcoin2/support/feed/time_window_median.h"
#include "coin2/app/driver.h"
#include "coin2/base/config/app_config.h"
#include "coin2/base/config/args.h"
#include "coin2/exchange/base/order/order.h"
#include "coin2/strategy/strategy.h"
#include "vitro.h"  // NOLINT

using namespace coin2::app;
using namespace coin2::base::config;
using coin2::exchange::base::feed::FeedUpdate;
using coin2::exchange::feed::FeedSystemType;
using presto::math::TimeWindowMedian;

struct Strategy : public ::coin2::strategy::IStrategy {
  Strategy(int64_t window_size_sec, double threshold_sec) {
    window_size = window_size_sec * 1e9;
    book_tds.ResizeWindow(window_size);
    trd_tds.ResizeWindow(window_size);
    book_tds_npopped = trd_tds_npopped = 0L;
    threshold_ns = threshold_sec * 1e9;
  }
  void onTradeFeed(const FeedUpdate& upd) override {
    UpdateWindow(
        &trd_tds,
        &trd_td_times,
        &trd_td_meds,
        &trd_tds_npopped,
        upd.timestamp(),
        upd.trade().timestamp,
        upd);
  }
  void onBookFeed(const FeedUpdate& upd) override {
    UpdateWindow(
        &book_tds,
        &book_td_times,
        &book_td_meds,
        &book_tds_npopped,
        upd.timestamp(),
        upd.book().Timestamp(),
        upd);
    if (upd.book().Bid0() && upd.book().Ask0()) {
      ftimes.push_back(upd.timestamp());
      etimes.push_back(upd.book().Timestamp());
      bidps.push_back(upd.book().Bid0()->price);
      askps.push_back(upd.book().Ask0()->price);
      midps.push_back(0.5 * (bidps.back() + askps.back()));
      if (book_td_meds.size() > 0) {
        if (!max_td || max_td < book_td_meds.back()) {
          max_td_time_begin = book_td_times.back() - window_size;
          max_td_time_end = book_td_times.back() + window_size;
          max_td = book_td_meds.back();
        }
      }
    }
  }

  void UpdateWindow(
      TimeWindowMedian<double>* td_window,
      std::vector<int64_t>* td_times,
      std::vector<double>* td_meds,
      int64_t* prev_num_popped,
      int64_t fetched_time,
      int64_t exchange_time,
      const FeedUpdate& upd) {
    CHECK_GT(exchange_time, 0) << "has no exchange time";
    int64_t elem = fetched_time - exchange_time;
    td_window->Update(fetched_time, elem);
    if (td_window->num_popped > *prev_num_popped) {
      td_times->push_back(fetched_time);
      td_meds->push_back(td_window->GetMedian());
      td_window->clear();
      *prev_num_popped = td_window->num_popped;
    }
  }

  std::tuple<
      std::vector<int64_t>,  // ftimes
      std::vector<int64_t>,  // etimes
      std::vector<double>>   // midps
  GetMaxTdRange() const {
    auto iter_i = std::lower_bound(ftimes.begin(), ftimes.end(), *max_td_time_begin);
    auto iter_j = std::lower_bound(ftimes.begin(), ftimes.end(), *max_td_time_end);
    int i = std::distance(ftimes.begin(), iter_i);
    int j = std::distance(ftimes.begin(), iter_j);

    std::vector<int64_t> ftimes_clip(ftimes.begin() + i, ftimes.begin() + j);
    std::vector<int64_t> etimes_clip(etimes.begin() + i, etimes.begin() + j);
    std::vector<double> midps_clip(midps.begin() + i, midps.begin() + j);
    return std::make_tuple(ftimes_clip, etimes_clip, midps_clip);
  }

  bool HitThreshold() const { return (max_td && *max_td > threshold_ns); }

  int64_t window_size;
  double threshold_ns;
  TimeWindowMedian<double> book_tds;
  TimeWindowMedian<double> trd_tds;
  int64_t book_tds_npopped;
  int64_t trd_tds_npopped;
  std::vector<int64_t> book_td_times;
  std::vector<double> book_td_meds;
  std::vector<int64_t> trd_td_times;
  std::vector<double> trd_td_meds;

  std::vector<int64_t> ftimes;
  std::vector<int64_t> etimes;
  std::vector<double> bidps;
  std::vector<double> askps;
  std::vector<double> midps;

  std::optional<int64_t> max_td_time_begin;
  std::optional<int64_t> max_td_time_end;
  std::optional<double> max_td;
};

void ParseOptionsIntoConfig2(const cxxopts::ParseResult& res, DriverConfig* app) {
  ParseDriverOptions(res, app);
  ParseProductOptions(res, app->mutable_feed());
  auto date = ParseDateOption(res);

  // fastfeed-conversion only option
  {
    auto& interval = *app->mutable_feed()->mutable_common()->mutable_archive()->mutable_interval();
    interval.set_start(std::to_string(date));

    if (res.count("duration") != 0U) {
      auto duration_str = res["duration"].as<std::string>();
      auto duration = DurationFromString(duration_str);
      interval.set_end(std::to_string(date + duration));
    } else {
      interval.set_end(std::to_string(date + 24 * 60 * 60 * 1'000'000'000LL));
    }
  }

  CHECK_EQ_THROW(res.count("mea"), 1) << "[Error] provide --mea";
  CHECK_EQ_THROW(res.count("symbol"), 1) << "[Error] provide --symbol";
  CHECK_EQ_THROW(res.count("recipe"), 1) << "[Error] provide --recipe";
  auto mea_str = res["mea"].as<std::string>();
  auto symbol_str = res["symbol"].as<std::string>();
  auto recipe_str = res["recipe"].as<std::string>();
  auto machine_str = res["machine"].as<std::string>();
  auto worker = res["worker"].as<int32_t>();

  auto mea = MarketExchangeApi::FromString(mea_str);
  app->mutable_feed()->mutable_common()->set_type(FeedSystemType::FEED_SYSTEM_TYPE_ARCHIVE);
  app->mutable_feed()->mutable_common()->mutable_archive()->set_machine(machine_str);
  app->mutable_feed()->mutable_common()->mutable_archive()->set_archive_type(
      coin2::exchange::feed::ArchiveConfig_ArchiveType_FASTFEED);
  app->mutable_feed()->mutable_common()->mutable_archive()->set_worker(worker);

  auto* exchanges = app->mutable_feed()->mutable_exchanges();
  (*exchanges)[mea_str].mutable_products()->add_norms(symbol_str);
  (*exchanges)[mea_str].set_recipe(recipe_str);
}

int main(int argc, char* argv[]) {
  /* ----------------------------------- opt ---------------------------------- */

  cxxopts::Options opt("Feed Dump", "Dump trade and bbo prices");
  opt.add_options()("symbol", "symbol str", cxxopts::value<std::string>());
  opt.add_options()("machine", "", cxxopts::value<std::string>());
  opt.add_options()("out_file", "", cxxopts::value<std::string>()->default_value("out.png"));
  opt.add_options()("window_size_sec", "", cxxopts::value<int64_t>()->default_value("720"));
  opt.add_options()("threshold_sec", "", cxxopts::value<double>()->default_value("5"));

  SetupOptions(&opt);
  auto res = opt.parse(argc, argv);
  ::coin2::app::DriverConfig app;
  ParseOptionsIntoConfig2(res, &app);

  LOG(INFO) << app.DebugString();

  /* ----------------------------------- run ---------------------------------- */
  printf("reading ..\n");

  Strategy strategy(res["window_size_sec"].as<int64_t>(), res["threshold_sec"].as<double>());
  ::coin2::app::Driver driver(app, &strategy);
  driver.Run();

  std::string key = fmt::format(
      "{} {} {}.{} {}",
      res["date"].as<std::string>(),
      res["machine"].as<std::string>(),
      res["mea"].as<std::string>(),
      res["recipe"].as<std::string>(),
      res["symbol"].as<std::string>());

  Figure fig(2, 1);
  fig.title = key;
  fig.width = 1200;
  fig.height = 1000;
  {
    auto& ax = fig.axes(1, 1);
    ax.is_x_nanotimestamps = true;
    ax.grid = true;
    {
      auto& line = ax.line("book_et_minus_ft", strategy.book_td_times, strategy.book_td_meds);
      line.alpha = 0.8;
      line.width = 0.2;
      line.drawstyle = "steps-post";
    }
  }
  if (!strategy.HitThreshold()) return 0;
  auto fem_tuple = strategy.GetMaxTdRange();
  {
    auto& ax = fig.axes(2, 1);
    ax.is_x_nanotimestamps = true;
    ax.grid = true;
    {
      auto& line = ax.line("ft vs midp", std::get<0>(fem_tuple), std::get<2>(fem_tuple));
      line.alpha = 0.8;
      line.width = 0.5;
      line.drawstyle = "steps-post";
    }
    {
      auto& line = ax.line("et vs midp", std::get<1>(fem_tuple), std::get<2>(fem_tuple));
      line.alpha = 0.8;
      line.width = 0.5;
      line.drawstyle = "steps-post";
    }
  }

  std::string out_file = res["out_file"].as<std::string>();

  Matplot matplot(fig);
  matplot.save(out_file);

  return 0;
}
